package nachos.userprog;

import nachos.machine.*;
import nachos.threads.*;
import nachos.userprog.*;

import java.io.EOFException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;

/**
 * Encapsulates the state of a user process that is not contained in its
 * user thread (or threads). This includes its address translation state, a
 * file table, and information about the program being executed.
 *
 * <p>
 * This class is extended by other classes to support additional functionality
 * (such as additional syscalls).
 *
 * @see	nachos.vm.VMProcess
 * @see	nachos.network.NetProcess
 */
public class UserProcess {
    /**
     * Allocate a new process.
     */
	
	
    public UserProcess() {
		//int numPhysPages = Machine.processor().getNumPhysPages();
		/*pageTable = new TranslationEntry[numPhysPages];
		for (int i = 0; i<stackPages; i++){
			pageTable[i] = new TranslationEntry(i,i, true,false,false,false);
		}*/
		
	//Group 7
        //Makes the first and second file descriptors be standard in and standard out (per assignment requirements)
        openFiles[0]  = UserKernel.console.openForReading();
        openFiles[1]  = UserKernel.console.openForWriting();

        //Assign a process number (0 is the first and is considered root)
        //used for handleHalt() to determine which is the root process
        mutex.P();
        processNumber = nextProcessNumber++;
        totalProcesses++;
        Lib.debug(dbgProcess, "---- Creating process " + processNumber);
        mutex.V();
        
        //Group 7 - initialize list of child processes
        children = new LinkedList<UserProcess>();

    }
    
    /**
     * Allocate and return a new process of the correct class. The class name
     * is specified by the <tt>nachos.conf</tt> key
     * <tt>Kernel.processClassName</tt>.
     *
     * @return	a new process of the correct class.
     */
    public static UserProcess newUserProcess() {
	return (UserProcess)Lib.constructObject(Machine.getProcessClassName());
    }

    /**
     * Execute the specified program with the specified arguments. Attempts to
     * load the program, and then forks a thread to run it.
     *
     * @param	name	the name of the file containing the executable.
     * @param	args	the arguments to pass to the executable.
     * @return	<tt>true</tt> if the program was successfully executed.
     */
    public boolean execute(String name, String[] args) {
	if (!load(name, args))
	    return false;
	
	new UThread(this).setName(name).fork();
	
	return true;
    }

    /**
     * Save the state of this process in preparation for a context switch.
     * Called by <tt>UThread.saveState()</tt>.
     */
    public void saveState() {
    }

    /**
     * Restore the state of this process after a context switch. Called by
     * <tt>UThread.restoreState()</tt>.
     */
    public void restoreState() {
	Machine.processor().setPageTable(UserKernel.getProcessPageTable(this.processNumber));
    }

    /**
     * Read a null-terminated string from this process's virtual memory. Read
     * at most <tt>maxLength + 1</tt> bytes from the specified address, search
     * for the null terminator, and convert it to a <tt>java.lang.String</tt>,
     * without including the null terminator. If no null terminator is found,
     * returns <tt>null</tt>.
     *
     * @param	vaddr	the starting virtual address of the null-terminated
     *			string.
     * @param	maxLength	the maximum number of characters in the string,
     *				not including the null terminator.
     * @return	the string read, or <tt>null</tt> if no null terminator was
     *		found.
     */
    public String readVirtualMemoryString(int vaddr, int maxLength) {
	Lib.assertTrue(maxLength >= 0);

	byte[] bytes = new byte[maxLength+1];

	int bytesRead = readVirtualMemory(vaddr, bytes);

	for (int length=0; length<bytesRead; length++) {
	    if (bytes[length] == 0)
		return new String(bytes, 0, length);
	}

	return null;
    }

    /**
     * Transfer data from this process's virtual memory to all of the specified
     * array. Same as <tt>readVirtualMemory(vaddr, data, 0, data.length)</tt>.
     *
     * @param	vaddr	the first byte of virtual memory to read.
     * @param	data	the array where the data will be stored.
     * @return	the number of bytes successfully transferred.
     */
    public int readVirtualMemory(int vaddr, byte[] data) {
	return readVirtualMemory(vaddr, data, 0, data.length);
    }

    /**
     * Transfer data from this process's virtual memory to the specified array.
     * This method handles address translation details. This method must
     * <i>not</i> destroy the current process if an error occurs, but instead
     * should return the number of bytes successfully copied (or zero if no
     * data could be copied).
     *
     * @param	vaddr	the first byte of virtual memory to read.
     * @param	data	the array where the data will be stored.
     * @param	offset	the first byte to write in the array.
     * @param	length	the number of bytes to transfer from virtual memory to
     *			the array.
     * @return	the number of bytes successfully transferred.
     */
    //Group7 start
    public int readVirtualMemory(int vaddr, byte[] data, int offset,
				 int length) 
    {
		Lib.assertTrue(offset >= 0 && length >= 0 && offset+length <= data.length);
		TranslationEntry[] pageTable = UserKernel.getProcessPageTable(this.processNumber);
		int amountRead = 0;
		byte[] memory = Machine.processor().getMemory();
		
		try{
			while(amountRead < length){
				//get the physical address:
				//first, check if virtual page is in our address space
				int vpn = vaddr /pageSize;
				if(vpn < 0 || vpn >= pageTable.length){
					Lib.debug(dbgProcess, "returning early from read, tried to read from "
							+ " an address we do not own");
					return amountRead;
				}
				int paddr = getPhysicalAddress(vaddr);
				if (paddr < 0 || paddr >= memory.length){
					Lib.debug(dbgProcess, "returning early from read, physical address was: "
								+ paddr + ", memory length is " + memory.length);
				    return amountRead;
				}
				//we should only read until the end of the page, as we do 
				// not know where the next physical page may be. Will have
				//to recalculate the virtual address after that. 
				int amount = Math.min(length, pageSize - length);
				System.arraycopy(memory, paddr, data, offset, amount);
				amountRead = amountRead + amount;
				vaddr = vaddr + amount;
				length = length - amount;
				offset = offset + amount;
			}
		}catch(IOException e){
			Lib.debug(dbgProcess, "IOException in getPhysicalAddress: " + e);
		}
		return amountRead;
    }
    // Group7 end
    
    /**
     * Transfer data from this process's virtual memory to the specified array.
     * This method handles address translation details. This method must
     * <i>not</i> destroy the current process if an error occurs, but instead
     * should return the number of bytes successfully copied (or zero if no
     * data could be copied).
     *
     * @param	vaddr	the first byte of virtual memory to read.
     * @param	data	the array where the data will be stored.
     * @param	offset	the first byte to write in the array.
     * @param	length	the number of bytes to transfer from virtual memory to
     *			the array.
     * @return	the number of bytes successfully transferred.
     */
    public int readVirtualMemoryORIGINAL(int vaddr, byte[] data, int offset,
				 int length) {
	Lib.assertTrue(offset >= 0 && length >= 0 && offset+length <= data.length);

	byte[] memory = Machine.processor().getMemory();
	
	// for now, just assume that virtual addresses equal physical addresses
	if (vaddr < 0 || vaddr >= memory.length)
	    return 0;

	int amount = Math.min(length, memory.length-vaddr);
	System.arraycopy(memory, vaddr, data, offset, amount);

	return amount;
    }

    /**
     * Transfer all data from the specified array to this process's virtual
     * memory.
     * Same as <tt>writeVirtualMemory(vaddr, data, 0, data.length)</tt>.
     *
     * @param	vaddr	the first byte of virtual memory to write.
     * @param	data	the array containing the data to transfer.
     * @return	the number of bytes successfully transferred.
     */
    public int writeVirtualMemory(int vaddr, byte[] data) {
	return writeVirtualMemory(vaddr, data, 0, data.length);
    }

    /**
     * Transfer data from the specified array to this process's virtual memory.
     * This method handles address translation details. This method must
     * <i>not</i> destroy the current process if an error occurs, but instead
     * should return the number of bytes successfully copied (or zero if no
     * data could be copied).
     *
     * @param	vaddr	the first byte of virtual memory to write.
     * @param	data	the array containing the data to transfer.
     * @param	offset	the first byte to transfer from the array.
     * @param	length	the number of bytes to transfer from the array to
     *			virtual memory.
     * @return	the number of bytes successfully transferred.
     */
    //Group 7 start
    public int writeVirtualMemory(int vaddr, byte[] data, int offset,
				  int length) {
		Lib.assertTrue(offset >= 0 && length >= 0 && offset+length <= data.length);
	
		TranslationEntry[] pageTable = UserKernel.getProcessPageTable(this.processNumber);
		int amountWritten = 0;
		byte[] memory = Machine.processor().getMemory();
		
		try{
			while(amountWritten < length){
				int vpn = vaddr /pageSize;
				//first, check if virtual page is in our address space
				if(vpn < 0 || vpn >= pageTable.length){
					Lib.debug(dbgProcess, "returning early from write, tried to write to "
							+ " an address we do not own");
					return amountWritten;
				}
				//check if the virtual address is in the read-only section
				if(pageTable[vpn].readOnly == true){
					Lib.debug(dbgProcess, "returning early from write, tried to write" +
							" to read-only memory");
					return amountWritten;
				}
				
				//get the physical address:
				int paddr = getPhysicalAddress(vaddr);
				if (paddr < 0 || paddr >= memory.length){
					Lib.debug(dbgProcess, "returning early from write, physical address was: "
								+ paddr + ", memory length is " + memory.length);
				    return amountWritten;
				}
				//we should only read until the end of the page, as we do 
				// not know where the next physical page may be. Will have
				//to recalculate the virtual address after that. 
				int amount = Math.min(length, pageSize - length);
				System.arraycopy(data, offset, memory, paddr, amount);
				amountWritten = amountWritten + amount;
				vaddr = vaddr + amount;
				length = length - amount;
				offset = offset + amount;
			}
		}catch(IOException e){
			Lib.debug(dbgProcess, "IOException in getPhysicalAddress: " + e);
		}
		return amountWritten;
	
    }
    //Group 7 end
    
    /**
     * Transfer data from the specified array to this process's virtual memory.
     * This method handles address translation details. This method must
     * <i>not</i> destroy the current process if an error occurs, but instead
     * should return the number of bytes successfully copied (or zero if no
     * data could be copied).
     *
     * @param	vaddr	the first byte of virtual memory to write.
     * @param	data	the array containing the data to transfer.
     * @param	offset	the first byte to transfer from the array.
     * @param	length	the number of bytes to transfer from the array to
     *			virtual memory.
     * @return	the number of bytes successfully transferred.
     */
    public int writeVirtualMemoryORIGINAL(int vaddr, byte[] data, int offset,
				  int length) {
	Lib.assertTrue(offset >= 0 && length >= 0 && offset+length <= data.length);

	byte[] memory = Machine.processor().getMemory();
	
	// for now, just assume that virtual addresses equal physical addresses
	if (vaddr < 0 || vaddr >= memory.length)
	    return 0;

	int amount = Math.min(length, memory.length-vaddr);
	System.arraycopy(data, offset, memory, vaddr, amount);

	return amount;
    }

    /**
     * Load the executable with the specified name into this process, and
     * prepare to pass it the specified arguments. Opens the executable, reads
     * its header information, and copies sections and arguments into this
     * process's virtual memory.
     *
     * @param	name	the name of the file containing the executable.
     * @param	args	the arguments to pass to the executable.
     * @return	<tt>true</tt> if the executable was successfully loaded.
     */
    private boolean load(String name, String[] args) {
	Lib.debug(dbgProcess, "UserProcess.load(\"" + name + "\")");
	
	OpenFile executable = ThreadedKernel.fileSystem.open(name, false);
	if (executable == null) {
	    Lib.debug(dbgProcess, "\topen failed");
	    return false;
	}

	try {
	    coff = new Coff(executable);
	}
	catch (EOFException e) {
	    executable.close();
	    Lib.debug(dbgProcess, "\tcoff load failed");
	    return false;
	}

	// make sure the sections are contiguous and start at page 0
	numPages = 0;
	for (int s=0; s<coff.getNumSections(); s++) {
	    CoffSection section = coff.getSection(s);
	    if (section.getFirstVPN() != numPages) {
		coff.close();
		Lib.debug(dbgProcess, "\tfragmented executable");
		return false;
	    }
	    numPages += section.getLength();
	}

	// make sure the argv array will fit in one page
	byte[][] argv = new byte[args.length][];
	int argsSize = 0;
	for (int i=0; i<args.length; i++) {
	    argv[i] = args[i].getBytes();
	    // 4 bytes for argv[] pointer; then string plus one for null byte
	    argsSize += 4 + argv[i].length + 1;
	}
	if (argsSize > pageSize) {
	    coff.close();
	    Lib.debug(dbgProcess, "\targuments too long");
	    return false;
	}

	// program counter initially points at the program entry point
	initialPC = coff.getEntryPoint();	

	// next comes the stack; stack pointer initially points to top of it
	numPages += stackPages;
	initialSP = numPages*pageSize;

	// and finally reserve 1 page for arguments
	numPages++;

	if (!loadSections())
	    return false;

	// store arguments in last page
	int entryOffset = (numPages-1)*pageSize;
	int stringOffset = entryOffset + args.length*4;

	this.argc = args.length;
	this.argv = entryOffset;
	
	for (int i=0; i<argv.length; i++) {
	    byte[] stringOffsetBytes = Lib.bytesFromInt(stringOffset);
	    Lib.assertTrue(writeVirtualMemory(entryOffset,stringOffsetBytes) == 4);
	    entryOffset += 4;
	    Lib.assertTrue(writeVirtualMemory(stringOffset, argv[i]) ==
		       argv[i].length);
	    stringOffset += argv[i].length;
	    Lib.assertTrue(writeVirtualMemory(stringOffset,new byte[] { 0 }) == 1);
	    stringOffset += 1;
	}

	return true;
    }

    /**
     * Allocates memory for this process, and loads the COFF sections into
     * memory. If this returns successfully, the process will definitely be
     * run (this is the last step in process initialization that can fail).
     *
     * @return	<tt>true</tt> if the sections were successfully loaded.
     */
    protected boolean loadSections() {
    	Lib.debug(dbgProcess, "Number of pages: " + numPages);
	//if (numPages > Machine.processor().getNumPhysPages()) {
		
    	//Group7:
    Lib.debug(dbgProcess, "free memory size: " + UserKernel.getFreeMemorySize());
  //if (numPages > Machine.processor().getNumPhysPages()) {
    if (numPages > UserKernel.getFreeMemorySize()){
	    coff.close();
	    Lib.debug(dbgProcess, "\tinsufficient physical memory");
	    return false;
	}    
    
    // Group7
    UserKernel.createProcessTable(this.processNumber, numPages);
	for (int i = 0; i<numPages; i++){
		int ppn = (Integer) UserKernel.getFreePage();
		UserKernel.getProcessPageTable(processNumber)[i] = new TranslationEntry(i,ppn, true,false,false,false);
		Lib.debug(dbgProcess, "Allocating vpn: " + i + " ppn " + ppn);
	}
    
    //

	// load sections
	for (int s=0; s<coff.getNumSections(); s++) {
	    CoffSection section = coff.getSection(s);
	    
	    Lib.debug(dbgProcess, "\tinitializing " + section.getName()
		      + " section (" + section.getLength() + " pages)");
	    Lib.debug(dbgProcess, "first VPN:  " + section.getFirstVPN());
			     
	    for (int i=0; i<section.getLength(); i++) {
	    	int vpn = section.getFirstVPN()+i;
	    	int ppn = UserKernel.getProcessPageTable(processNumber)[vpn].ppn;
	    	Lib.debug(dbgProcess, "vpn:  " + vpn + ", ppn: " + ppn);
	    	//Group7
	    	//Mark the entry in TranslationEntry readOnly as needed. 
	    	if(section.isReadOnly()){
	    		UserKernel.getProcessPageTable(processNumber)[vpn].readOnly = true;
	    		Lib.debug(dbgProcess, "page " + vpn + " is read only");
	    	}
	    	
	    	//Group7
	    	section.loadPage(i, ppn);
	    }
	    Lib.debug(dbgProcess, "Loaded all sections");
	}
	
	return true;
    }

    /**
     * Release any resources allocated by <tt>loadSections()</tt>.
     */
    protected void unloadSections() {
    	TranslationEntry[] pageTable = UserKernel.getProcessPageTable(processNumber);
    	//Group7 start
    	for(int i= 0; i < pageTable.length; i++){
    		UserKernel.returnPage(pageTable[i].ppn);
    	}
    	//Group7 end
    }    
    
    /**
     * Group 7
     * terminate the process and release all resources
     */
    protected void terminate(){
    	unloadSections();
    	UserKernel.destroyProcessTable(processNumber);
    	mutex.P();
    	Lib.debug(dbgProcess, "---- Destroying process " + processNumber);
    	totalProcesses--;
    	mutex.V();
    	
    }
    

    /**
     * Initialize the processor's registers in preparation for running the
     * program loaded into this process. Set the PC register to point at the
     * start function, set the stack pointer register to point at the top of
     * the stack, set the A0 and A1 registers to argc and argv, respectively,
     * and initialize all other registers to 0.
     */
    public void initRegisters() {
	Processor processor = Machine.processor();

	// by default, everything's 0
	for (int i=0; i<processor.numUserRegisters; i++)
	    processor.writeRegister(i, 0);

	// initialize PC and SP according
	processor.writeRegister(Processor.regPC, initialPC);
	processor.writeRegister(Processor.regSP, initialSP);

	// initialize the first two argument registers to argc and argv
	processor.writeRegister(Processor.regA0, argc);
	processor.writeRegister(Processor.regA1, argv);
    }

    

    /**
     * Handle the halt() system call.
     */
    private int handleHalt() {
    	
        //Group 7 - Ensure that only the root process (process 0) can call this.
        if(processNumber != 0) return -1;
        Machine.halt();

	Lib.assertNotReached("Machine.halt() did not halt machine!");
	return 0;
    }

    /*Group 7
     * 
     * Handles the exit() syscall.
     * 
     * Orphans any children processes.
     * Cleans up file descriptors and frees memory.
     * Checks to see if this thread should be joined to a parent.
     * Exits the current process immediately (terminates machine if this is the last process)
     * 
     */
    
    private int handleExit(int status) {
    	System.out.println("Process # " + this.processNumber + " exiting with status of " + status);
    	children = null;
    	this.terminate();
    	
        if (parent != null){
            parent.joinLock.acquire();
            if (parent.waitingToJoin == this.processNumber){
            	this.status = status;
            	parent.joinCondition.wake();
            }
            parent.joinLock.release();
        }
    	
    	if(totalProcesses == 0){
    		Kernel.kernel.terminate();
    		Lib.assertNotReached("Machine.halt() did not halt machine!");
    	}
        
        UThread.currentThread().finish();
    	return 0;
    }    
    
    /* Group 7
     * 
	 * Handles the exec() system call.
	 * 
	 * Checks that the number of arguments (numArgs) is greater than or equal to 0.
	 * Checks that this is a valid program name. 
	 * Runs process, returns the processId of the child
	 * 
	 */
    
    private int handleExec(int f, int numArgs, int a) {
    	//assert that numArgs is non-negative
    	System.out.println("in handle exec: numargs: " + numArgs + " argument is: " + a);
    	String progName = this.readVirtualMemoryString(f, ADDRESS_LENGTH);
    	if(progName == null) return -1;
    	
    	//args
    	if(numArgs < 0) return -1;
    	
    	String[] args = new String[numArgs];
    	int addr = a;
    	for(int i=0;i<args.length;i++){
    	    byte[] data = new byte[4]; 
    	    int bytesRead = this.readVirtualMemory(addr, data);
    	    int ptrArgv = Lib.bytesToInt(data, 0);
    	    args[i] = this.readVirtualMemoryString(ptrArgv, ADDRESS_LENGTH);
    	    System.out.println("args: " + args[i]);
    	    addr += 4;
    	}
    	
    	UserProcess child = createProcess(stackPages);
    	this.children.add(child);
    	child.parent = this;
    	Lib.assertTrue(child.execute(progName, args));
    	
    	return child.processNumber;
    }    
    
    /*
     * Group 7
     * 
     * Handles the join() system call.
     * 
     * Looks for child that is waiting to be joined (throws error if none can be found);
     * Wakes up the parent process
     * Handles the status returned from the child process
     */

    private int handleJoin(int processId, int status) {
    	Lib.debug(dbgProcess, "process " + this.processNumber + " waiting to join child " + processId);
    	//Find child first, throw error if can't find it
    	joinLock.acquire();
    	UserProcess child = null;
    	
    	for(UserProcess u : children){
    		if(u.processNumber == processId){
    			child = u;
    		}else{
    			//can't find child
    			joinLock.release();
    			return -1;
    		}
    	}
    	
		waitingToJoin = processId;
		joinCondition.sleep();
    	
    	if (child.getStatus() != 99){
	        int numberOfBytesWritten = writeVirtualMemory(status, Lib.bytesFromInt(child.getStatus()));
	        if (numberOfBytesWritten != 4){
	        	joinLock.release();
	            return -1;
	        }
        }    	
    	
    	joinLock.release();
    	if(child == null) return -1;
    	children.remove(child);
    	
    	return 0;
    }       
    
    private int getStatus() {
		return status;
	}

	//Group 7

    /* create and open the filename being pointed to add the specified address
     * If the filename is null, return -1
     * If the file won't open, return -1
     * If we can't open the file because we are already at our max open files, return -1
     * Otherwise, return the index of the file descrptor.
     */
    private int handleCreate(int address){
        String name = this.readVirtualMemoryString(address, ADDRESS_LENGTH);
        if(name == null) return -1;
        OpenFile o = UserKernel.fileSystem.open(name, true);
        if(o == null) return -1;
        int fileIndex = this.getFileIndex();
        if(fileIndex == -1) return -1;
        openFiles[fileIndex] = o;

        return fileIndex;

    }
    //Group 7
    //Close the file at the given fileDescriptor index
    //if the fileDescriptor is < 0 or > the maximum open files (16) return -1
    //If the file at the given descriptor doesn't exist, return -1
    //Otherise, return 0
    private int handleClose(int fileDescriptor){
        if(fileDescriptor < 0 || fileDescriptor >= MAX_OPEN_FILES) return -1;

        OpenFile o = openFiles[fileDescriptor];
        if(o == null) return -1;

        o.close();
        openFiles[fileDescriptor] = null;


        return 0;
    }


     //Group 7

    /* open the filename being pointed to add the specified address
     * If the filename is null, return -1
     * If the file won't open, return -1
     * If we can't open the file because we are already at our max open files, return -1
     * Otherwise, return the index of the file descrptor.
     * This is identical to handleCreate but we aren't creating the file, just opening.
     */
    private int handleOpen(int address){
        String name = this.readVirtualMemoryString(address, ADDRESS_LENGTH);
        if(name == null) return -1;

        OpenFile o = UserKernel.fileSystem.open(name, false);
        if(o == null) return -1;

        int fileIndex = this.getFileIndex();
        if(fileIndex == -1) return -1;
        openFiles[fileIndex] = o;

        return fileIndex;


    }
    /*
     * Group 7
     * Read byteCount bytes from the file identified by the fileDescriptor into the
     * buffer at bufferAddress.
     * if the fileDescriptor is < 0 or > the maximum open files (16) return -1
     * If the file at the given descriptor doesn't exist, return -1
     * If the bytes read from the file into the buffer is -1, return -1
     * If there was a problem transferring the buffer to memory, return -1
     * Otherwise, return the number of bytes transferred to memory.
     */
    private int handleRead(int fileDescriptor,int bufferAddress,int byteCount){
         if(fileDescriptor < 0 || fileDescriptor >= MAX_OPEN_FILES) return -1;

         if (byteCount < 0) return -1;

         OpenFile o = openFiles[fileDescriptor];

         if (o == null) return -1;

         byte [] buffer = new byte[byteCount];

         int bytesRead = o.read(buffer, 0, byteCount);
         
         if (bytesRead == -1 ) return -1;

         int bytesTransferred = this.writeVirtualMemory(bufferAddress, buffer, 0, bytesRead);

         if(bytesTransferred < 0) return -1;

         return bytesTransferred;
    }

    /*
     * Group 7
     * Write byteCount bytes from the buffer at bufferAddress to the file identified by fileDescriptor
     * if the fileDescriptor is < 0 or > the maximum open files (16) return -1
     * If the file at the given descriptor doesn't exist, return -1
     * If there was a problem reading from memory into the buffer, return -1
     * If there was a problem writing to the file, return -1
     * If the number of bytes written is less than the bytes requested to be written, return -1
     * Otherwise, return the number of bytes written to disk.
     */
     protected int handleWrite(int fileDescriptor,int bufferAddress,int byteCount){
    	 Lib.debug(dbgProcess, "--------- In handleWrite");
    	 if(fileDescriptor < 0 || fileDescriptor >= MAX_OPEN_FILES) return -1;
          if (byteCount < 0) return -1;

          OpenFile o = openFiles[fileDescriptor];

          if (o == null) return -1;

          byte [] buffer = new byte[byteCount];

          int bytesRead = this.readVirtualMemory(bufferAddress, buffer, 0, byteCount);

          if(bytesRead < byteCount) return -1;

          int bytesWritten = o.write(buffer, 0, bytesRead);
          
          System.out.println("Process # " + processNumber + " " + new String(buffer));
          
          if(bytesWritten < byteCount) return  -1;

          return bytesWritten;

     }

     /* Group 7
      * Remove the filename being pointed to be the specified address
      * If name is null, return -1
      * If it was not removed successfully, return -1
      * If it was removed successfully, return 1.
      */
     public int handleUnlink(int address){
         String name = this.readVirtualMemoryString(address, ADDRESS_LENGTH);
         if(name == null) return -1;

         boolean removed = UserKernel.fileSystem.remove(name);

         if(!removed) return -1;

         return 0;
     }


    //Group 7
    //Find the lowest available index to use for the file
    //return -1 if all spots are taken
    protected int getFileIndex(){
        for(int i = 0; i < openFiles.length; i++){
            if(openFiles[i] == null) return i;
        }
        return -1;
    }


    protected static final int
    syscallHalt = 0,
	syscallExit = 1,
	syscallExec = 2,
	syscallJoin = 3,
	syscallCreate = 4,
	syscallOpen = 5,
	syscallRead = 6,
	syscallWrite = 7,
	syscallClose = 8,
	syscallUnlink = 9;

    /**
     * Handle a syscall exception. Called by <tt>handleException()</tt>. The
     * <i>syscall</i> argument identifies which syscall the user executed:
     *
     * <table>
     * <tr><td>syscall#</td><td>syscall prototype</td></tr>
     * <tr><td>0</td><td><tt>void halt();</tt></td></tr>
     * <tr><td>1</td><td><tt>void exit(int status);</tt></td></tr>
     * <tr><td>2</td><td><tt>int  exec(char *name, int argc, char **argv);
     * 								</tt></td></tr>
     * <tr><td>3</td><td><tt>int  join(int pid, int *status);</tt></td></tr>
     * <tr><td>4</td><td><tt>int  creat(char *name);</tt></td></tr>
     * <tr><td>5</td><td><tt>int  open(char *name);</tt></td></tr>
     * <tr><td>6</td><td><tt>int  read(int fd, char *buffer, int size);
     *								</tt></td></tr>
     * <tr><td>7</td><td><tt>int  write(int fd, char *buffer, int size);
     *								</tt></td></tr>
     * <tr><td>8</td><td><tt>int  close(int fd);</tt></td></tr>
     * <tr><td>9</td><td><tt>int  unlink(char *name);</tt></td></tr>
     * </table>
     * 
     * @param	syscall	the syscall number.
     * @param	a0	the first syscall argument.
     * @param	a1	the second syscall argument.
     * @param	a2	the third syscall argument.
     * @param	a3	the fourth syscall argument.
     * @return	the value to be returned to the user.
     */
    public int handleSyscall(int syscall, int a0, int a1, int a2, int a3) {
    switch (syscall) {
	case syscallHalt:
	    return handleHalt();
	case syscallExit:
		return handleExit(a0);
	case syscallExec:
		return handleExec(a0,a1,a2);
	case syscallJoin:
		return handleJoin(a0,a1);
    case syscallCreate://Group7
        return handleCreate(a0);
    case syscallClose://Group7
        return handleClose(a0);
    case syscallOpen://Group7
        return handleOpen(a0);
    case syscallRead://Group7
        return handleRead(a0,a1,a2);
    case syscallWrite://Group7
        return handleWrite(a0,a1,a2);
    case syscallUnlink://Group7
        return handleUnlink(a0);

	default:
	    Lib.debug(dbgProcess, "Unknown syscall " + syscall);
	    Lib.assertNotReached("Unknown system call!");
	}
	return 0;
    }

    /**
     * Handle a user exception. Called by
     * <tt>UserKernel.exceptionHandler()</tt>. The
     * <i>cause</i> argument identifies which exception occurred; see the
     * <tt>Processor.exceptionZZZ</tt> constants.
     *
     * @param	cause	the user exception that occurred.
     */
    public void handleException(int cause) {
	Processor processor = Machine.processor();

	switch (cause) {
	case Processor.exceptionSyscall:
	    int result = handleSyscall(processor.readRegister(Processor.regV0),
				       processor.readRegister(Processor.regA0),
				       processor.readRegister(Processor.regA1),
				       processor.readRegister(Processor.regA2),
				       processor.readRegister(Processor.regA3)
				       );
	    processor.writeRegister(Processor.regV0, result);
	    processor.advancePC();
	    break;				       
				       
	default:
	    Lib.debug(dbgProcess, "Unexpected exception: " +
		      Processor.exceptionNames[cause]);
	    Lib.assertNotReached("Unexpected exception");
	}
    }
    
    private int getPhysicalAddress(int vaddr)throws IOException{
    	TranslationEntry[] pageTable = UserKernel.getProcessPageTable(processNumber);
    	//first get the virtual page which contains this address:
    	int vpn = Processor.pageFromAddress(vaddr); 
    	//now get the physical page:
    	if(vpn > pageTable.length){
    		//illegal memory access. This page is not in this process pageTable. 
    		// we throw exception
    		throw new IOException("Illegal memory access");
    	}
		int ppn = pageTable[vpn].ppn;
		//now get the offset in the page:
		int offset = vaddr % pageSize;
		//now get the processor to calculate the physical address and return it:
		
		return Processor.makeAddress(ppn, offset);
    }
    
    public static void selfTest(){
    	UserProcess up = createProcess(4);
    	int vaddress = 50;
    	 
    	TranslationEntry[] pageTable = UserKernel.getProcessPageTable(up.processNumber);
    	Lib.debug(dbgProcess, "Free pages before self-test: " + UserKernel.getFreeMemorySize());
    	Lib.debug(dbgProcess, "\n starting user process self test, num of pages is 4");	
    	Lib.debug(dbgProcess, "\n----- pass test case, memory is r/w in our space: ");	
    	byte[] dataOut = Lib.bytesFromInt(23);
    	Lib.debug(dbgProcess, "bytes to write: " + dataOut.length + " data to write: 23" );
    	int bytesProcessed = up.writeVirtualMemory(vaddress, dataOut); 
    	Lib.debug(dbgProcess, "wrote this many bytes: " + bytesProcessed);
    	//check if we can read it now
    	
    	byte[] dataIn = new byte[dataOut.length];
    	int bytesRead = up.readVirtualMemory(vaddress, dataIn);
    	Lib.debug(dbgProcess, "read this many bytes: " + bytesRead);
    	int result = Lib.bytesToInt(dataIn, 0);
    	Lib.debug(dbgProcess, "data read in: " + result);
    	
    	
    	//now make one user entry read-only and try writing:
    	Lib.debug(dbgProcess, "\n ----- fail test case for write, read-only memory in our space ");
    	pageTable[2].readOnly = true;
    	vaddress = pageSize * 2 + 50;
    	dataOut = Lib.bytesFromInt(52);
    	Lib.debug(dbgProcess, "bytes to write: " + dataOut.length +  " number is " + 52);
    	bytesProcessed = up.writeVirtualMemory(vaddress, dataOut);
    	Lib.debug(dbgProcess, "wrote this many bytes: " + bytesProcessed);
    	dataIn = new byte[dataOut.length];
    	bytesRead = up.readVirtualMemory(vaddress, dataIn);
    	Lib.debug(dbgProcess, "read this many bytes: " + bytesRead);
    	result = Lib.bytesToInt(dataIn, 0);
    	Lib.debug(dbgProcess, "data read in from read only " + result);
    	
    	
    	//now try writing to a space that we do not own:
    	Lib.debug(dbgProcess, "\n ----- fail test case, memory not in our space: ");
    	vaddress = pageSize * 6 + 50;
    	bytesProcessed = up.writeVirtualMemory(vaddress, dataOut);
    	Lib.debug(dbgProcess, "wrote this many bytes: " + bytesProcessed);
    	dataIn = new byte[dataOut.length];
    	bytesRead = up.readVirtualMemory(vaddress, dataIn);
    	Lib.debug(dbgProcess, "read this many bytes: " + bytesRead + "\n");
    	    	    	
    	up.terminate();
    	
    	Lib.debug(dbgProcess, "Free pages after first test: " + UserKernel.getFreeMemorySize());
    	
    	//now just test loading the proccesses. There are 64 
    	//pages total, so load an uload the processes until memory wraps up:
    	
    	Lib.debug(dbgProcess, "total pages available: " + Machine.processor().getNumPhysPages());
    	int userCount = 8;
    	UserProcess[] users = new UserProcess[userCount];
    	for(int i = 0; i < userCount; i++){
    		users[i] = createProcess(8);
    		
    	}
    	Lib.debug(dbgProcess, userCount + " users with 8 pages loaded");
    	for(int i = 0; i < userCount; i++){
    		if(users[i] != null)
    		users[i].terminate();
    		users[i] = null;
    	}
    	Lib.debug(dbgProcess, userCount + " users with 8 pages unloaded");
    	Lib.debug(dbgProcess, "Free pages after 8-user test: " + UserKernel.getFreeMemorySize());
    	
    	//lets create lots of small proccess now so we will get some gaps 
    	userCount = 30;
    	users = new UserProcess[userCount];
    	for(int i = 0; i < userCount; i++){
    		users[i] = createProcess(2);
    		
    	}
    	Lib.debug(dbgProcess, userCount + " users with 2 pages loaded");
    	
    	//lets unload some users, so we will force some "gaps" in free memory
    	for(int i = 0; i < users.length; i++){
    		if(i%2 == 0){
    			if(users[i] != null)
    			users[i].terminate();
    			users[i] = null;
    		}
    	}
    	
    	Lib.debug(dbgProcess, "Free after 30 user test: " + UserKernel.getFreeMemorySize());
    	
    	Lib.debug(dbgProcess,  15 + " users with 2 pages unloaded");
    	
    	userCount = 4;
    	UserProcess[] users2 = new UserProcess[userCount];
    	for(int i = 0; i < userCount; i++){
    		users2[i] = createProcess(6);
    		
    	}
    	Lib.debug(dbgProcess,  userCount + " users with 6 pages loaded");
    	for(int i = 0; i < 2; i++){
    		if(users2[i] != null)
    		users2[i].terminate();
    		users2[i] = null;
    	}
    	Lib.debug(dbgProcess, "2 users with 6 pages unloaded");
    	Lib.debug(dbgProcess, "Free pages after 2 user test: " + UserKernel.getFreeMemorySize());
    	
    	userCount = 2;
    	for(int i = 0; i < userCount; i++){
    		users2[i] = createProcess(8);
    	}
    	Lib.debug(dbgProcess,  userCount + " users with 8 pages loaded");
    	for(int i = 0; i < users.length; i++){
    		if(i%2 != 0){
    			if(users[i] != null)
    			users[i].terminate();
    			users[i] = null;
    		}
    	}
    	for(int i = 0; i < users2.length; i++){
    		if(users[i] != null)
    		users2[i].terminate();
    		users2[i] = null;
    	}
    	
    	Lib.debug(dbgProcess, "all users unloaded");
    	
    	System.out.println("Free pages after self-test: " + UserKernel.getFreeMemorySize());
    	//now let's just reset the process id
    	nextProcessNumber = 0;
    }
   
    private static UserProcess createProcess(int pages){
    	UserProcess up = new UserProcess();
    	if(pages > UserKernel.getFreeMemorySize())
    		return null;
      	UserKernel.createProcessTable(up.processNumber, pages);
    	return up;
    }

    protected final int ADDRESS_LENGTH = 256;
    private final int MAX_OPEN_FILES = 16;
    protected OpenFile[] openFiles = new OpenFile[MAX_OPEN_FILES];
    protected int processNumber;
    private static int totalProcesses;
    public static int nextProcessNumber = 0;

    /** The program being run by this process. */
    protected Coff coff;

    /** This process's page table. */
   
    /** The number of contiguous pages occupied by the program. */
    protected int numPages;

    /** The number of pages in the program's stack. */
    protected final int stackPages = 8;
    
    private int initialPC, initialSP;
    private int argc, argv;
	
    //Group 7
    protected UserProcess parent;
    protected LinkedList<UserProcess> children;
    
    private static final int pageSize = Processor.pageSize;
    private static final char dbgProcess = 'a';
    private static Semaphore mutex = new Semaphore(1);

    //Group 7
    private int waitingToJoin;	//pid of the child process this process is waiting for
    private int status = 99;	//use 99 as a dummy status since 0 is meaningful	
    private Lock joinLock = new Lock();
    private Condition2 joinCondition = new Condition2(joinLock);
}
